package org.zjvis.datascience.service.snapshot;


import com.google.common.base.Preconditions;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import lombok.NonNull;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Service;
import org.zjvis.datascience.common.dto.DashboardDTO;
import org.zjvis.datascience.common.widget.dto.WidgetDTO;
import org.zjvis.datascience.service.DashboardService;
import org.zjvis.datascience.service.WidgetService;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Service
public class DashboardSnapshotManagerService {
    private static final int MAX_DASHBOARDS_SNAPSHOT_PER_USER = 3;
    private static final int MAX_STEPS_SNAPSHOT_PER_DASHBOARD = 7;
    private static final int MINUTES_TO_EXPIRE = 15;
    private static final String DASHBOARD_SNAPSHOT_MANAGER_CACHE_KEY = "dashboard_snapshot_manager_cache";
    private final WidgetService widgetService;
    private final DashboardService dashboardService;

    public DashboardSnapshotManagerService(WidgetService widgetService, DashboardService dashboardService) {
        this.widgetService = widgetService;
        this.dashboardService = dashboardService;
    }

    public void update(HttpServletRequest httpServletRequest, DashboardDTO dashboard) throws ExecutionException {
        SnapshotManager<DashboardSnapshot> snapshotManager = extractSnapshotManager(
                httpServletRequest, dashboard.getId().toString());
        snapshotManager.snapshot(DashboardSnapshot.builder()
                .dashboardDTO(dashboard)
                .widgetDTOList(widgetService.queryAllWidgetsByDashboardId(dashboard.getId()))
                .build());
    }

    public void redo(HttpServletRequest httpServletRequest, Long dashboardId)
            throws SnapshotManager.SnapshotException, ExecutionException {
        SnapshotManager<DashboardSnapshot> snapshotManager = extractSnapshotManager(
                httpServletRequest, dashboardId.toString());
        snapshotManager.redo();
        syncSnapshotToDatabase(snapshotManager.current(), dashboardId);
    }

    public void undo(HttpServletRequest httpServletRequest, Long dashboardId)
            throws SnapshotManager.SnapshotException, ExecutionException {
        SnapshotManager<DashboardSnapshot> snapshotManager = extractSnapshotManager(
                httpServletRequest, dashboardId.toString());
        snapshotManager.undo();
        syncSnapshotToDatabase(snapshotManager.current(), dashboardId);
    }

    public boolean notSnapshotBefore(HttpServletRequest httpServletRequest, DashboardDTO dashboard)
            throws ExecutionException {
        SnapshotManager<DashboardSnapshot> snapshotManager = extractSnapshotManager(
                httpServletRequest, dashboard.getId().toString());
        return snapshotManager.notSnapshotBefore();
    }

    public boolean sameAsSnapshot(HttpServletRequest httpServletRequest, DashboardDTO dashboard)
            throws ExecutionException {
        SnapshotManager<DashboardSnapshot> snapshotManager = extractSnapshotManager(
                httpServletRequest, dashboard.getId().toString());
        DashboardSnapshot dashboardSnapshot = snapshotManager.current();
        if (!dashboardEquals(dashboard, dashboardSnapshot.getDashboardDTO())) {
            return false;
        }
        return widgetListEquals(
                widgetService.queryAllWidgetsByDashboardId(dashboard.getId()),
                dashboardSnapshot.getWidgetDTOList()
        );
    }

    // TODO move out
    public void initialization(HttpServletRequest request, Long dashboardId) throws ExecutionException {
        if (dashboardId != null) {
            if (dashboardId != -1 && dashboardId != -2 && dashboardId != 0) {
                if (notSnapshotBefore(request, dashboardService.queryById(dashboardId)))
                    update(request, dashboardService.queryById(dashboardId));
            }
        }
    }

    @NotNull
    private SnapshotManager<DashboardSnapshot> extractSnapshotManager(
            HttpServletRequest httpServletRequest,
            String dashboardIdentify
    ) throws ExecutionException {
        LoadingCache<String, SnapshotManager<DashboardSnapshot>> managerLoadingCache
                = cacheFromSession(httpServletRequest.getSession());
        return managerLoadingCache.get(dashboardIdentify);
    }

    private LoadingCache<String, SnapshotManager<DashboardSnapshot>> cacheFromSession(HttpSession httpSession) {
        Object attribute = httpSession.getAttribute(DASHBOARD_SNAPSHOT_MANAGER_CACHE_KEY);
        if (null != attribute) {
            Preconditions.checkArgument(
                    attribute instanceof LoadingCache,
                    "attribute(%s) is not an instance of %s",
                    attribute, LoadingCache.class.getName()
            );
            return (LoadingCache<String, SnapshotManager<DashboardSnapshot>>)
                    httpSession.getAttribute(DASHBOARD_SNAPSHOT_MANAGER_CACHE_KEY);
        }
        LoadingCache<String, SnapshotManager<DashboardSnapshot>> snapshotManagerLoadingCache = CacheBuilder.newBuilder()
                .maximumSize(MAX_DASHBOARDS_SNAPSHOT_PER_USER)
                .expireAfterAccess(MINUTES_TO_EXPIRE, TimeUnit.MINUTES)
                .build(new CacheLoader<String, SnapshotManager<DashboardSnapshot>>() {
                    @Override
                    @NonNull
                    public SnapshotManager<DashboardSnapshot> load(@NonNull String dashboardId) {
                        return new SnapshotManager<>(MAX_STEPS_SNAPSHOT_PER_DASHBOARD);
                    }
                });
        httpSession.setAttribute(DASHBOARD_SNAPSHOT_MANAGER_CACHE_KEY, snapshotManagerLoadingCache);
        return snapshotManagerLoadingCache;
    }

    private void syncSnapshotToDatabase(DashboardSnapshot dashboardSnapshot, Long dashboardId) {
        DashboardDTO dashboardDTOFromSnapshot = dashboardSnapshot.getDashboardDTO();
        dashboardService.update(dashboardDTOFromSnapshot);
        List<WidgetDTO> widgetDTOListFromSnapshot = dashboardSnapshot.getWidgetDTOList();
        List<WidgetDTO> widgetDTOListFromDatabase = widgetService.queryAllWidgetsByDashboardId(dashboardId);
        for (WidgetDTO widgetDTO : widgetDTOListFromSnapshot) {
            widgetService.update(widgetDTO);
        }
        Set<Long> widgetDtoIdListFromSnapshot = widgetDTOListFromSnapshot.stream()
                .map(WidgetDTO::getId)
                .collect(Collectors.toSet());
        for (WidgetDTO widgetDTO : widgetDTOListFromDatabase) {
            Long widgetDTOId = widgetDTO.getId();
            if (!widgetDtoIdListFromSnapshot.contains(widgetDTOId)) {
                widgetService.deleteById(widgetDTOId);
            }
        }
    }

    private boolean dashboardEquals(DashboardDTO dashboard, DashboardDTO dashboardSnapshot) {
        // TODO: should use equals method
        if (null == dashboard) {
            return null == dashboardSnapshot;
        }
        return Objects.equals(dashboard.getId(), dashboardSnapshot.getId())
                && Objects.equals(dashboard.getProjectId(), dashboardSnapshot.getProjectId())
                && Objects.equals(dashboard.getLayout(), dashboardSnapshot.getLayout())
                && Objects.equals(dashboard.getPublishLayout(), dashboardSnapshot.getPublishLayout())
                // TODO publish time
                // TODO publish no
                && Objects.equals(dashboard.getStatus(), dashboardSnapshot.getStatus())
                && Objects.equals(dashboard.getName(), dashboardSnapshot.getName())
                && Objects.equals(dashboard.getType(), dashboardSnapshot.getType())
                && Objects.equals(dashboard.getIsComplex(), dashboardSnapshot.getIsComplex())
                && Objects.equals(dashboard.getShowWatermark(), dashboardSnapshot.getShowWatermark())
                && Objects.equals(dashboard.getMappingJson(), dashboardSnapshot.getMappingJson())
                && Objects.equals(dashboard.getVisitorIds(), dashboardSnapshot.getVisitorIds())
                && Objects.equals(dashboard.getBackgroundImg(), dashboardSnapshot.getBackgroundImg());
    }

    private boolean widgetListEquals(List<WidgetDTO> widgetDTOList, List<WidgetDTO> widgetDTOListSnapshot) {
        if (null == widgetDTOList) {
            return null == widgetDTOListSnapshot;
        }
        if (widgetDTOList.size() != widgetDTOListSnapshot.size()) {
            return false;
        }
        widgetDTOList.sort(Comparator.comparing(WidgetDTO::getId));
        widgetDTOListSnapshot.sort(Comparator.comparing(WidgetDTO::getId));
        for (int index = 0; index < widgetDTOList.size(); index++) {
            if (!widgetEquals(widgetDTOList.get(index), widgetDTOListSnapshot.get(index))) {
                return false;
            }
        }
        return true;
    }

    private boolean widgetEquals(WidgetDTO widgetDTO, WidgetDTO widgetDTOSnapshot) {
        if (null == widgetDTO) {
            return null == widgetDTOSnapshot;
        }
        return Objects.equals(widgetDTO.getId(), widgetDTOSnapshot.getId())
                && Objects.equals(widgetDTO.getTid(), widgetDTOSnapshot.getTid())
                && Objects.equals(widgetDTO.getType(), widgetDTOSnapshot.getType())
                && Objects.equals(widgetDTO.getName(), widgetDTOSnapshot.getName())
                && Objects.equals(widgetDTO.getDataJson(), widgetDTOSnapshot.getDataJson())
                && Objects.equals(widgetDTO.getPublishName(), widgetDTOSnapshot.getPublishName())
                && Objects.equals(widgetDTO.getPublishDataJson(), widgetDTOSnapshot.getPublishDataJson())
                && Objects.equals(widgetDTO.getPublishTime(), widgetDTOSnapshot.getPublishTime())
                && Objects.equals(widgetDTO.getStatus(), widgetDTOSnapshot.getStatus())
                && Objects.equals(widgetDTO.getDashboardId(), widgetDTOSnapshot.getDashboardId())
                && Objects.equals(widgetDTO.getProjectId(), widgetDTOSnapshot.getProjectId());
    }
}
